package com.github.facadefs.local;

import com.github.facadefs.FileSystem;
import com.github.facadefs.FileView;
import com.github.facadefs.Operations;
import com.github.facadefs.Selector;
import com.github.facadefs.util.Operation;
import com.github.facadefs.util.Paths;

import java.io.*;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.*;
import java.util.*;

/**
 * 基于本地文件系统的文件系统实现.
 *
 * @author vacoor
 * @since 1.0
 */
public class LocalFileSystem implements FileSystem {
    private static final boolean WINDOWS = System.getProperty("os.name").toLowerCase().startsWith("windows");

    /**
     * Change Root.
     */
    private final String chroot;

    public LocalFileSystem() {
        this(null);
    }

    public LocalFileSystem(String chrootDirectory) {
        if (WINDOWS && Paths.ROOT.equals(chrootDirectory)) {
            chrootDirectory = null;
        }
        if (null != chrootDirectory) {
            final File chrootFile = new File(chrootDirectory);
            if (!chrootFile.isAbsolute()) {
                throw new IllegalArgumentException("chroot must be is a absolute path");
            }
            if (!chrootFile.exists() && !chrootFile.mkdirs()) {
                throw new IllegalArgumentException("chroot is not exists");
            }
            if (!chrootFile.isDirectory()) {
                throw new IllegalArgumentException("chroot must be is a directory");
            }
        }
        this.chroot = chrootDirectory;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean exists(final String path) {
        return null != stat(path);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FileView stat(final String path) {
        return stat0(toAbsolutePath(path));
    }

    private FileView stat0(final String absPath) {
        final File file = new File(absPath);
        return file.exists() ? toView(file) : null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FileView[] ls(final String path) {
        final String absPath = this.toAbsolutePath(path);
        final FileView stat = stat0(absPath);
        /*- FIXME windows 磁盘会返回 null
        if (null == stat) {
            return new FileView[0];
        }
        */
        if (null != stat && !stat.isDirectory()) {
            return new FileView[]{stat};
        }

        File[] entries = new File(absPath).listFiles();
        entries = null != entries ? entries : new File[0];
        final FileView[] stats = new FileView[entries.length];
        for (int i = 0; i < entries.length; i++) {
            stats[i] = toView(entries[i]);
        }
        return stats;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void ls(final String path, final Selector selector) throws IOException {
        final String absPath = this.toAbsolutePath(path);
        final File target = new File(absPath);
        /*- FIXME windows 磁盘会返回 null
        if (null == stat) {
            return new FileView[0];
        }
        */
        if (!target.exists()) {
            return;
        }
        if (!target.isDirectory()) {
            final FileView view = toView(target);
            selector.select(view);
            return;
        }

        final DirectoryStream<Path> children = Files.newDirectoryStream(target.toPath());
        try {
            for (final Path child : children) {
                selector.select(toView(child.toFile()));
            }
        } finally {
            children.close();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean mkdirs(final String path) throws IOException {
        final String absPath = toAbsolutePath(path);
        return mkdirs(new File(absPath));
    }

    /**
     * 如果目录已经创建成功或已经存在返回 true, 创建失败返回 false.
     * 如果已经存在但不是一个目录则抛出异常.
     *
     * @throws IOException 如果发生 IO 异常
     */
    protected boolean mkdirs(final File path) throws IOException {
        if (path.exists()) {
            if (!path.isDirectory()) {
                throw new IOException("File already exists and is not directory: " + path);
            }
            return true;
        }
        return path.mkdirs();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void rm(final String path, final boolean recurse) throws IOException {
        final String absPath = toAbsolutePath(path);
        final File target = new File(absPath);
        if (!target.exists()) {
            return;
        }
        if (target.isDirectory()) {
            File[] entries = target.listFiles();
            entries = null != entries ? entries : new File[0];
            if (!recurse && 0 < entries.length) {
                throw new IOException("You cannot delete non-empty directory, use recurse=true to override");
            }

            for (final File entry : entries) {
                final String name = entry.getName();
                if (Paths.isCurrentDirName(name) || Paths.isParentDirName(name)) {
                    continue;
                }
                rm(entry.getAbsolutePath(), recurse);
            }
        }
        if (!target.delete()) {
            throw new IOException("cannot delete file: " + target);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void rename(final String oldPath, final String newPath) throws IOException {
        final String oldAbsPath = toAbsolutePath(oldPath);
        final String newAbsPath = toAbsolutePath(newPath);

        // FIXME 目录重命名
        if (!new File(oldAbsPath).renameTo(new File(newAbsPath))) {
            throw new IOException("rename failed: " + oldAbsPath + " -> " + newAbsPath);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public InputStream open(final String path) throws IOException {
        final String absPath = toAbsolutePath(path);
        final File target = new File(absPath);
        if (!target.exists()) {
            return null;
        }
        if (target.isDirectory()) {
            throw new IOException("Can't open " + path + " because it is a directory");
        }
        return new FileInputStream(target);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public OutputStream create(final String path, boolean override) throws IOException {
        return create(path, null, override);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public OutputStream create(final String path, final String contentType, final boolean override) throws IOException {
        final String absPath = toAbsolutePath(path);
        final File target = new File(absPath);
        if (target.exists()) {
            if (!override) {
                throw new IOException("File already exists: " + path);
            } else if (target.isDirectory()) {
                throw new IOException("Can't override " + path + " because it is a directory");
            }
        } else if (!target.getParentFile().exists() && !mkdirs(target.getParentFile())) {
            throw new IOException("mkdirs failed to create: " + target.getParent());
        }
        return new FileOutputStream(target);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <R> R execute(final Operation<Operations, R> operation) throws IOException {
        return operation.execute(this);
    }

    private String toAbsolutePath(String path) {
        return Paths.toAbsolutePath(this.chroot, path);
    }

    private String toRelativePath(final String absPath) {
        return Paths.toRelativePath(this.chroot, absPath);
    }

    private FileView toView(final File file) {
        if (!file.exists()) {
            return null;
        }

        final FileView view = !WINDOWS ? newUnixView(file) : newDosView(file);
        if (null == view) {
            final String path = toRelativePath(file.getAbsolutePath());
            final long length = file.length();
            final boolean isDir = file.isDirectory();
            final long atime = -1;
            final long mtime = -1;
            final int mode = mode(file);
            return new FileView(path, length, isDir, mtime, atime, mode, null, null, null);
        }
        return view;
    }

    private FileView newUnixView(final File file) {
        try {
            final Path path = file.toPath();
            final boolean isDirectory = Files.isDirectory(path);
            final Map<String, ?> attrsMap = Files.readAttributes(path, "unix:*");

            final long atime = ((FileTime) attrsMap.get("lastAccessTime")).toMillis();
            final long mtime = ((FileTime) attrsMap.get("lastModifiedTime")).toMillis();
            final String owner = ((UserPrincipal) attrsMap.get("owner")).getName();
            final String group = ((GroupPrincipal) attrsMap.get("group")).getName();
            final int mode = (Integer) attrsMap.get("mode");
            final String symlink = tryReadSymbolicLink(path);
            final String symlinkToUse = null != symlink ? toRelativePath(symlink) : null;
            return new FileView(toRelativePath(file.getAbsolutePath()), file.length(), isDirectory, mtime, atime, mode, owner, group, symlinkToUse);
        } catch (final IOException ignore) {
            // ignore
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    private FileView newDosView(final File f) {
        try {
            final Path path = f.toPath();
            final boolean isDirectory = Files.isDirectory(path);
            final Map<String, ?> dosAttrsMap = Files.readAttributes(path, "dos:*");
            final Map<String, ?> aclAttrsMap = Files.readAttributes(path, "acl:*");

            final long atime = ((FileTime) dosAttrsMap.get("lastAccessTime")).toMillis();
            final long mtime = ((FileTime) dosAttrsMap.get("lastModifiedTime")).toMillis();
            final String owner = ((UserPrincipal) aclAttrsMap.get("owner")).getName();

            final int mode = mode(f);

            final List<AclEntry> acl = (List<AclEntry>) aclAttrsMap.get("acl");
            final List<AclEntry> aclToUse = null != acl ? acl : Collections.<AclEntry>emptyList();
            final Set<String> groupNames = new HashSet<String>();
            for (final AclEntry entry : aclToUse) {
                if (!AclEntryType.ALLOW.equals(entry.type())) {
                    continue;
                }

                /*-
                <pre>
                int entryMode = 0;
                Set<AclEntryPermission> permissions = entry.permissions();
                if (permissions.contains(AclEntryPermission.READ_DATA)) {
                    entryMode |= FileView.GROUP_READ;
                }
                if (permissions.contains(AclEntryPermission.WRITE_DATA)) {
                    entryMode |= FileView.GROUP_WRITE;
                }
                if (permissions.contains(AclEntryPermission.EXECUTE)) {
                    entryMode |= FileView.GROUP_EXECUTE;
                }
                if (0 < entryMode) {
                    mode &= entryMode;
                }
                </pre>
                */
                groupNames.add(entry.principal().getName());
            }

            final String group = groupNames.toString();
            final String symlink = tryReadSymbolicLink(path);
            final String symlinkToUse = null != symlink ? toRelativePath(symlink) : null;

            return new FileView(toRelativePath(f.getAbsolutePath()), f.length(), isDirectory, mtime, atime, mode, owner, group, symlinkToUse);
        } catch (final IOException ignore) {
            // ignore
            return null;
        }
    }

    private int mode(final File file) {
        int mode = 0;
        mode |= file.canRead() ? FileView.OWNER_READ : 0;
        mode |= file.canWrite() ? FileView.OWNER_WRITE : 0;
        mode |= file.canExecute() ? FileView.OWNER_EXECUTE : 0;
        return mode;
    }

    private String tryReadSymbolicLink(final Path path) {
        if (Files.isSymbolicLink(path)) {
            try {
                return Files.readSymbolicLink(path).toAbsolutePath().toString();
            } catch (final IOException ignore) {
                // ignore
            }
        }
        return null;
    }

}
